/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wolf.core.util;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;

/**
 * 日期函数
 * 
 * @author 何小锋
 * 
 */
public class DateUtil {

	public static enum DateUnit {
		YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
	}

	public static TimeZone UTC = TimeZone.getTimeZone("UTC");
	public static final Locale US = Locale.US;

	private static String NOW = "NOW";
	private static char Z = 'Z';
	private static Pattern splitter = Pattern.compile("\\b|(?<=\\d)(?=\\D)");

	private static ThreadLocal<ISO8601DateFormat> local = new ThreadLocal<DateUtil.ISO8601DateFormat>() {
		protected ISO8601DateFormat initialValue() {
			return new ISO8601DateFormat();
		};
	};

	/**
	 * 增加日期
	 * 
	 * @param c
	 * @param val
	 * @param unit
	 */
	public static void add(Calendar c, int val, DateUnit unit) {
		if (c == null || unit == null)
			return;
		switch (unit) {
		case YEAR:
			c.add(Calendar.YEAR, val);
			break;
		case MONTH:
			c.add(Calendar.MONTH, val);
			break;
		case DAY:
			c.add(Calendar.DATE, val);
			break;
		case HOUR:
			c.add(Calendar.HOUR_OF_DAY, val);
			break;
		case MINUTE:
			c.add(Calendar.MINUTE, val);
			break;
		case SECOND:
			c.add(Calendar.SECOND, val);
			break;
		}
	}

	/**
	 * 日期按指定单位取整
	 * 
	 * @param c
	 * @param unit
	 */
	public static void round(Calendar c, DateUnit unit) {
		if (c == null || unit == null)
			return;

		switch (unit) {
		case YEAR:
			c.clear(Calendar.MONTH);
		case MONTH:
			c.clear(Calendar.DAY_OF_MONTH);
			c.clear(Calendar.DAY_OF_WEEK);
			c.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
			c.clear(Calendar.DAY_OF_YEAR);
			c.clear(Calendar.WEEK_OF_MONTH);
			c.clear(Calendar.WEEK_OF_YEAR);
		case DAY:
			c.clear(Calendar.HOUR_OF_DAY);
			c.clear(Calendar.HOUR);
			c.clear(Calendar.AM_PM);
		case HOUR:
			c.clear(Calendar.MINUTE);
		case MINUTE:
			c.clear(Calendar.SECOND);
		case SECOND:
			c.clear(Calendar.MILLISECOND);
			break;
		}

	}

	/**
	 * 解析日期
	 * 
	 * @param math
	 * @return
	 * @throws ParseException
	 */
	public static Date parse(String math) {
		return parse(math, null, null);
	}

	/**
	 * 解析日期
	 * 
	 * @param math
	 *            日期算数表达式
	 * @param timeZone
	 *            时区
	 * @param locale
	 *            语言
	 * @return
	 * @throws ParseException
	 */
	public static Date parse(String math, TimeZone timeZone, Locale locale) {
		if (timeZone == null)
			timeZone = UTC;
		if (locale == null)
			locale = US;
		Calendar cal = Calendar.getInstance(timeZone, locale);

		if (math == null || math.isEmpty()) {
			return cal.getTime();
		}

		if (math.startsWith(NOW)) {
			math = math.substring(NOW.length());
		} else {
			int zz = math.indexOf(Z);
			if (zz > 0) {
				try {
					cal.setTime(local.get().parse(math.substring(0, zz + 1)));
					math = math.substring(zz + 1);
				} catch (ParseException e) {
					throw new IllegalArgumentException("Invalid Date String:'"
							+ math + '\'');
				}
			} else {
				throw new IllegalArgumentException("Invalid Date String:'"
						+ math + '\'');
			}
		}

		String[] ops = splitter.split(math);
		int pos = 0;
		char command;
		int val;

		while (pos < ops.length) {
			// 判定当前是否是运算符
			if (1 != ops[pos].length()) {
				throw new IllegalArgumentException(
						"Multi character command found: \"" + ops[pos] + "\"");
			}
			// 得到当前的运算符，并指向下一个字符串
			command = ops[pos++].charAt(0);

			// 判定运算符
			switch (command) {
			case '/':
				// 判定后续是否有单位
				if (ops.length < pos + 1) {
					throw new IllegalArgumentException(
							"Need a unit after command: \"" + command + "\"");
				}
				try {
					// 取整，并指向下一个字符串
					round(cal, DateUnit.valueOf(ops[pos++]));
				} catch (Exception e) {
					throw new IllegalArgumentException(
							"Unit not recognized: \"" + ops[pos - 1] + "\"");
				}
				break;
			case '+':
			case '-':
				// 判定后续是否有数值及单位
				if (ops.length < pos + 2) {
					throw new IllegalArgumentException(
							"Need a value and unit for command: \"" + command
									+ "\"");
				}
				val = 0;
				try {
					val = Integer.valueOf(ops[pos++]);
				} catch (Exception e) {
					throw new IllegalArgumentException("Not a Number: \""
							+ ops[pos - 1] + "\"");
				}
				if ('-' == command) {
					val = 0 - val;
				}
				try {
					add(cal, val, DateUnit.valueOf(ops[pos++]));
				} catch (Exception e) {
					throw new IllegalArgumentException(
							"Unit not recognized: \"" + ops[pos - 1] + "\"");
				}
				break;
			default:
				throw new IllegalArgumentException("Unrecognized command: \""
						+ command + "\"");
			}
		}

		return cal.getTime();
	}

	private static class ISO8601DateFormat extends SimpleDateFormat {

		private static final long serialVersionUID = 1L;

		protected NumberFormat millisParser = NumberFormat
				.getIntegerInstance(US);
		protected NumberFormat millisFormat = new DecimalFormat(".###",
				new DecimalFormatSymbols(US));

		public ISO8601DateFormat() {
			super("yyyy-MM-dd'T'HH:mm:ss", US);
			this.setTimeZone(UTC);
		}

		@Override
		public Date parse(String text, ParsePosition p) {
			Date d = super.parse(text, p);
			int milliIndex = p.getIndex();
			if (null != d && -1 == p.getErrorIndex()
					&& milliIndex + 1 < text.length()
					&& '.' == text.charAt(milliIndex)) {
				p.setIndex(++milliIndex);
				Number millis = millisParser.parse(text, p);
				if (-1 == p.getErrorIndex()) {
					int endIndex = p.getIndex();
					d = new Date(d.getTime()
							+ (long) (millis.doubleValue() * Math.pow(10,
									(3 - endIndex + milliIndex))));
				}
			}
			return d;
		}

		@Override
		public StringBuffer format(Date d, StringBuffer toAppendTo,
				FieldPosition pos) {
			super.format(d, toAppendTo, pos);
			long millis = d.getTime() % 1000l;
			if (0l == millis) {
				return toAppendTo;
			}
			int posBegin = toAppendTo.length();
			toAppendTo.append(millisFormat.format(millis / 1000d));
			if (DateFormat.MILLISECOND_FIELD == pos.getField()) {
				pos.setBeginIndex(posBegin);
				pos.setEndIndex(toAppendTo.length());
			}
			return toAppendTo;
		}

		@Override
		public Object clone() {
			ISO8601DateFormat c = (ISO8601DateFormat) super.clone();
			c.millisParser = NumberFormat.getIntegerInstance(US);
			c.millisFormat = new DecimalFormat(".###",
					new DecimalFormatSymbols(US));
			return c;
		}
	}

}
